package com.hilton.tigershark.downloads;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.app.ExpandableListActivity;
import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.AsyncQueryHandler;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.hilton.tigershark.R;
import com.hilton.tigershark.downloads.Downloads.DownloadStatus;

public class DownloadsActivity extends ExpandableListActivity {
    public static final String TAG = "DownloadsActivity";
    
    private static final long PROGRESS_REFRESH_INTERVAL = 100;
    private static final int REFRESH_PROGRESS = 10000;
    
    private static final int GET_ALL_DOWNLOADING = 11;
    private static final int GET_ALL_FAILED = 12;
    private static final int GET_ALL_COMPLETED = 13;

    protected static final int CLEAR_COMPLETED_NOTIFICATION = 100;
    
    private Cursor mCursorDownloading;
    private Cursor mCursorFailed;
    private Cursor mCursorCompleted;
    private MyContentObserver mContentObserver;
    private DownloadsAdapter mAdapter;
    private DownloadsAsyncQueryHandler mAsyncQueryHandler;
    private ExpandableListView mListView;
    private NotificationManager mNotificationManager;
    private ExecutorService mExecutor;
    private IDownloadsService mTheService;
    private HashMap<Integer, ProgressBar> mProgressTable;
    
    private Handler mHandler = new Handler() {
	@Override
	public void handleMessage(Message msg) {
	    switch (msg.what) {
	    case CLEAR_COMPLETED_NOTIFICATION:
		mExecutor.submit(new ClearNotification());
		break;
	    case REFRESH_PROGRESS:
		doRefreshProgress();
		break;
	    default:
		break;
	    }
	}
    };
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	mListView = getExpandableListView();
	mAsyncQueryHandler = new DownloadsAsyncQueryHandler(getContentResolver());
	mAdapter = new DownloadsAdapter(this, null, null, null);
	mContentObserver = new MyContentObserver(new Handler());
	mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
	mExecutor = Executors.newSingleThreadExecutor();
	mProgressTable = new HashMap<Integer, ProgressBar>();
	getDataFromDB();
	setListAdapter(mAdapter);
	
	bindToService();
    }

    private void getDataFromDB() {
	mAsyncQueryHandler.startQuery(GET_ALL_DOWNLOADING, null, Downloads.CONTENT_URI, Downloads.PROJECTION, 
		Downloads.STATUS + "=?", new String[]{String.valueOf(DownloadStatus.STATUS_DOWNLOADING)}, null);
	mAsyncQueryHandler.startQuery(GET_ALL_FAILED, null, Downloads.CONTENT_URI, Downloads.PROJECTION, 
		Downloads.STATUS + "=?", new String[]{String.valueOf(DownloadStatus.STATUS_FAILED)}, null);
	mAsyncQueryHandler.startQuery(GET_ALL_COMPLETED, null, Downloads.CONTENT_URI, Downloads.PROJECTION, 
		Downloads.STATUS + "=?", new String[]{String.valueOf(DownloadStatus.STATUS_COMPLETED)}, null);
    }
    
    @Override
    protected void onResume() {
	super.onResume();
	getContentResolver().registerContentObserver(Downloads.CONTENT_URI, false, mContentObserver);
    }
    
    @Override
    protected void onPause() {
	super.onPause();
	getContentResolver().unregisterContentObserver(mContentObserver);
    }
    
    @Override
    protected void onDestroy() {
	super.onDestroy();
	if (mCursorDownloading != null && !mCursorDownloading.isClosed()) {
	    mCursorDownloading.close();
	}
	mExecutor.shutdownNow();
	unbindFromService();
    }
    
    private void bindToService() {
	final Intent i = new Intent();
	i.setClass(getApplication(), DownloadService.class);
	bindService(i, mServiceConnection, Context.BIND_AUTO_CREATE);
    }
    
    private void unbindFromService() {
	if (mServiceConnection != null) {
	    unbindService(mServiceConnection);
	}
    }
    
    private ServiceConnection mServiceConnection = new ServiceConnection() {
	public void onServiceConnected(ComponentName className, IBinder obj) {
	    Log.e(TAG, "service " + className + " connected as " + obj);
	    mTheService = IDownloadsService.Stub.asInterface(obj);
	}
	
	public void onServiceDisconnected(ComponentName className) {
	    Log.e(TAG, "service " + className + " is disconnected");
	}
    };
    
    private class MyContentObserver extends ContentObserver {
	public MyContentObserver(Handler handler) {
	    super(handler);
	}

	@Override
	public boolean deliverSelfNotifications() {
	    Log.e(TAG, "deliverSelfNotifications self ");
	    return true;
	}

	@Override
	public void onChange(boolean selfChange) {
	    super.onChange(selfChange);
	    if (!selfChange) {
		// TODO: better way to update progress
		getDataFromDB();
	    }
	}
    }
    
    private class ClearNotification implements Runnable {
	@Override
	public void run() {
	    doClear(mCursorCompleted);
	    doClear(mCursorFailed);
	}

	private void doClear(Cursor c) {
	    if (!c.moveToFirst()) {
		return;
	    }
	    do {
		final int id = c.getInt(Downloads.ID_INDEX);
		mNotificationManager.cancel(id);
	    } while (c.moveToNext());
	}
    }
    
    private final class DownloadsAsyncQueryHandler extends AsyncQueryHandler {
	public DownloadsAsyncQueryHandler(ContentResolver cr) {
	    super(cr);
	}
	
	@Override
	protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
	    switch (token) {
	    case GET_ALL_DOWNLOADING:
		mCursorDownloading = cursor;
		startManagingCursor(mCursorDownloading);
		mAdapter.changeDownloadingCursor(mCursorDownloading);
		break;
	    case GET_ALL_FAILED:
		mCursorFailed = cursor;
		startManagingCursor(mCursorFailed);
		mAdapter.changeFailedCursor(mCursorFailed);
		clearNotifications();
		break;
	    case GET_ALL_COMPLETED:
		mCursorCompleted = cursor;
		startManagingCursor(mCursorCompleted);
		mAdapter.changeCompletedCursor(mCursorCompleted);
		clearNotifications();
		break;
	    default:
		throw new IllegalArgumentException("bad token for async query " + token);
	    }
	}
    }
    
    private void clearNotifications() {
	mHandler.removeMessages(CLEAR_COMPLETED_NOTIFICATION);
	mHandler.sendEmptyMessageDelayed(CLEAR_COMPLETED_NOTIFICATION, 100);
    }
    
    private class DownloadsAdapter extends BaseExpandableListAdapter {
	private LayoutInflater mFactory;
	private Cursor mGroupDownloading;
	private Cursor mGroupFailed;
	private Cursor mGroupCompleted;
	
	public DownloadsAdapter(Context ctx, Cursor dc, Cursor fc, Cursor cc) {
	    mFactory = LayoutInflater.from(ctx);
	    mGroupDownloading = dc;
	    mGroupFailed = fc;
	    mGroupCompleted = cc;
	}

	public void changeCompletedCursor(Cursor cc) {
	    if (mGroupCompleted != null) {
		mGroupCompleted.close();
	    }
	    mGroupCompleted = cc;
	    if (cc == null) {
		notifyDataSetInvalidated();
	    } else {
		notifyDataSetChanged();
	    }
	    refreshGroupStatus(2);
	}

	public void changeFailedCursor(Cursor fc) {
	    if (mGroupFailed != null) {
		mGroupFailed.close();
	    }
	    mGroupFailed = fc;
	    if (fc == null) {
		notifyDataSetInvalidated();
	    } else {
		notifyDataSetChanged();
	    }
	    refreshGroupStatus(1);
	}

	private void refreshGroupStatus(int groupPos) {
	    if (getChildrenCount(groupPos) > 0) {
		mListView.expandGroup(groupPos);
	    } else {
		mListView.collapseGroup(groupPos);
	    }
	}

	public void changeDownloadingCursor(Cursor dc) {
	    if (mGroupDownloading != null) {
		mGroupDownloading.close();
	    }
	    mGroupDownloading = dc;
	    if (dc == null) {
		notifyDataSetInvalidated();
	    } else {
		notifyDataSetChanged();
	    }
	    refreshGroupStatus(0);
	}

	@Override
	public Cursor getChild(int groupPosition, int childPosition) {
	    final Cursor c = getGroup(groupPosition);
	    c.moveToPosition(childPosition);
	    return c;
	}

	@Override
	public long getChildId(int groupPosition, int childPosition) {
	    return getChild(groupPosition, childPosition).getInt(Downloads.ID_INDEX);
	}

	@Override
	public View getChildView(int groupPosition, int childPosition,
		boolean isLastChild, View convertView, ViewGroup parent) {
	    View childView = convertView;
	    if (childView == null) {
		childView = mFactory.inflate(R.layout.download_item, null);
	    }
	    Cursor cursor = getChild(groupPosition, childPosition);
	    final int id = cursor.getInt(Downloads.ID_INDEX);
	    final ProgressBar prog = (ProgressBar) childView.findViewById(R.id.progress);
	    final int total = cursor.getInt(Downloads.TOTAL_BYTES_INDEX);
	    prog.setMax(total);
	    
	    final TextView dateLabel = (TextView) childView.findViewById(R.id.date);
	    final long date = cursor.getLong(Downloads.CREATED_INDEX);
	    dateLabel.setText(readableDateLabel(date));
	    
	    final TextView contentLength = (TextView) childView.findViewById(R.id.content_length);
	    contentLength.setText(humanReadableSize(total));
	    
	    final TextView filename = (TextView) childView.findViewById(R.id.filename);
	    filename.setText(cursor.getString(Downloads.FILENAME_INDEX));
	    
	    final String contentType = cursor.getString(Downloads.MIME_TYPE_INDEX);
	    final int status = cursor.getInt(Downloads.STATUS_INDEX);
	    final String path = cursor.getString(Downloads.PATH_INDEX);
	    if (status == DownloadStatus.STATUS_COMPLETED || status == DownloadStatus.STATUS_FAILED) {
		mProgressTable.remove(id);
		if (mProgressTable.isEmpty()) {
		    stopRefreshLoop();
		}
		prog.setVisibility(View.INVISIBLE);
		if (status == DownloadStatus.STATUS_COMPLETED) {
		    childView.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
			    final Intent i = new Intent(Intent.ACTION_VIEW);
			    i.setAction(Intent.ACTION_VIEW);
			    i.setDataAndType(Uri.parse("file://" + path), contentType);
			    i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
			    try {
				startActivity(i);
			    } catch (ActivityNotFoundException e) {
				Log.e(TAG, "lalala", e);
				Toast.makeText(DownloadsActivity.this, "Unable to open " + filename.getText(), Toast.LENGTH_SHORT).show();
			    }
			}
		    });
		}
	    } else {
		mProgressTable.put(id, prog);
		startRefreshLoop();
	    }
	    return childView;
	}
	
	@Override
	public int getChildrenCount(int groupPosition) {
	    switch (groupPosition) {
	    case 0:
		return mGroupDownloading == null ? 0 : mGroupDownloading.getCount();
	    case 1:
		return mGroupFailed == null ? 0 : mGroupFailed.getCount();
	    case 2:
		return mGroupCompleted == null ? 0 : mGroupCompleted.getCount();
	    default:
		throw new IllegalArgumentException("bad group position " + groupPosition);
	    }
	}

	@Override
	public Cursor getGroup(int groupPosition) {
	    switch (groupPosition) {
	    case 0:
		return mGroupDownloading;
	    case 1:
		return mGroupFailed;
	    case 2:
		return mGroupCompleted;
	    default:
		throw new IllegalArgumentException("bad group pos for get group " + groupPosition);
	    }
	}

	@Override
	public int getGroupCount() {
	    return 3;
	}

	@Override
	public long getGroupId(int groupPosition) {
	    return groupPosition;
	}

	@Override
	public View getGroupView(int groupPosition, boolean isExpanded,
		View convertView, ViewGroup parent) {
	    View groupView = convertView;
	    if (groupView == null) {
		groupView = mFactory.inflate(R.layout.download_group, null);
	    }
	    final String name = DownloadStatus.readableStatusLabel(groupPosition);
	    final int count = getChildrenCount(groupPosition);
	    final TextView groupInfoLabel = (TextView) groupView.findViewById(R.id.group_name);
	    groupInfoLabel.setText(name + "   (" + count + ")");
	    return groupView;
	}

	@Override
	public boolean hasStableIds() {
	    return true;
	}

	@Override
	public boolean isChildSelectable(int groupPosition, int childPosition) {
	    return true;
	}
    }
    
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MMM dd");


    
    /* TODO:
     * Today
     * Yesterday
     * mm dd
     * for date in last year
     * yyyy mm dd
     */
    private CharSequence readableDateLabel(long date) {
	Date d = new Date(date);
	return DATE_FORMAT.format(d);
    }
    
    private void startRefreshLoop() {
	mHandler.sendEmptyMessage(REFRESH_PROGRESS);
    }

    private void stopRefreshLoop() {
	mHandler.removeMessages(REFRESH_PROGRESS);
    }
    
    private void doRefreshProgress() {
	if (mTheService == null) {
	    Log.e(TAG, "doRefreshProgress Oops service is not connected, do nothing ");
	    return;
	}
	// iterate through progress table, update the progressbar by getting progress from service
	Iterator<Integer> iter = mProgressTable.keySet().iterator();
	while (iter.hasNext()) {
	    final int id = iter.next();
	    ProgressBar prog = mProgressTable.get(id);
	    if (prog == null) {
		Log.e(TAG, "bad value, progressbar is null. is there something wrong");
		continue;
	    }
	    int progress = 0;
	    try {
		progress = mTheService.getProgress(id);
	    } catch (RemoteException e) {
		Log.e(TAG, "remote exception ", e);
		continue;
	    }
	    prog.setProgress(progress);
	}
	mHandler.sendEmptyMessageDelayed(REFRESH_PROGRESS, PROGRESS_REFRESH_INTERVAL);
    }
    
    private static final int KILOBYTES = 1024;
    private static final int METABYTES = 1024 * 1024;
    
    private String humanReadableSize(int total) {
	if (total < KILOBYTES) {
	    return total + "B";
	} else if (total < METABYTES) {
	    return String.format("%.2fK", (float) total / (float) KILOBYTES);
	} else {
	    return String.format("%.2fM", (float) total / (float) METABYTES);
	}
    }
}
